home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 September / PCWorld_2008-09_cd.bin / v cisle / sadanastroju / lightning-0.8-tb-win.xpi / js / calRecurrenceInfo.js < prev    next >
Text File  |  2007-12-03  |  27KB  |  764 lines

  1. /* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is lightning code.
  16.  *
  17.  * The Initial Developer of the Original Code is
  18.  *  Oracle Corporation
  19.  * Portions created by the Initial Developer are Copyright (C) 2005
  20.  * the Initial Developer. All Rights Reserved.
  21.  *
  22.  * Contributor(s):
  23.  *   Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
  24.  *   Matthew Willis <lilmatt@mozilla.com>
  25.  *   Daniel Boelzle <daniel.boelzle@sun.com>
  26.  *
  27.  * Alternatively, the contents of this file may be used under the terms of
  28.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  29.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  30.  * in which case the provisions of the GPL or the LGPL are applicable instead
  31.  * of those above. If you wish to allow use of your version of this file only
  32.  * under the terms of either the GPL or the LGPL, and not to allow others to
  33.  * use your version of this file under the terms of the MPL, indicate your
  34.  * decision by deleting the provisions above and replace them with the notice
  35.  * and other provisions required by the GPL or the LGPL. If you do not delete
  36.  * the provisions above, a recipient may use your version of this file under
  37.  * the terms of any one of the MPL, the GPL or the LGPL.
  38.  *
  39.  * ***** END LICENSE BLOCK ***** */
  40.  
  41. function calRecurrenceInfo() {
  42.     this.mRecurrenceItems = new Array();
  43.     this.mExceptions = new Array();
  44. }
  45.  
  46. function calDebug() {
  47.     dump.apply(null, arguments);
  48. }
  49.  
  50. var calRecurrenceInfoClassInfo = {
  51.     getInterfaces: function (count) {
  52.         var ifaces = [
  53.             Components.interfaces.nsISupports,
  54.             Components.interfaces.calIRecurrenceInfo,
  55.             Components.interfaces.nsIClassInfo
  56.         ];
  57.         count.value = ifaces.length;
  58.         return ifaces;
  59.     },
  60.  
  61.     getHelperForLanguage: function (language) {
  62.         return null;
  63.     },
  64.  
  65.     contractID: "@mozilla.org/calendar/recurrence-info;1",
  66.     classDescription: "Calendar Recurrence Info",
  67.     classID: Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}"),
  68.     implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
  69.     flags: 0
  70. };
  71.  
  72. calRecurrenceInfo.prototype = {
  73.     // QI with CI
  74.     QueryInterface: function(aIID) {
  75.         if (aIID.equals(Components.interfaces.nsISupports) ||
  76.             aIID.equals(Components.interfaces.calIRecurrenceInfo))
  77.             return this;
  78.  
  79.         if (aIID.equals(Components.interfaces.nsIClassInfo))
  80.             return calRecurrenceInfoClassInfo;
  81.  
  82.         throw Components.results.NS_ERROR_NO_INTERFACE;
  83.     },
  84.  
  85.     //
  86.     // Mutability bits
  87.     //
  88.     mImmutable: false,
  89.     get isMutable() { return !this.mImmutable; },
  90.     makeImmutable: function() {
  91.         if (this.mImmutable)
  92.             return;
  93.  
  94.         for each (ritem in this.mRecurrenceItems) {
  95.             if (ritem.isMutable)
  96.                 ritem.makeImmutable();
  97.         }
  98.  
  99.         for each (ex in this.mExceptions) {
  100.             if (ex.item.isMutable)
  101.                 ex.item.makeImmutable();
  102.         }
  103.  
  104.         this.mImmutable = true;
  105.     },
  106.  
  107.     clone: function() {
  108.         var cloned = new calRecurrenceInfo();
  109.         cloned.mBaseItem = this.mBaseItem;
  110.  
  111.         var clonedItems = [];
  112.         for each (ritem in this.mRecurrenceItems)
  113.             clonedItems.push(ritem.clone());
  114.         cloned.mRecurrenceItems = clonedItems;
  115.  
  116.         var clonedExceptions = [];
  117.         for each (exitem in this.mExceptions) {
  118.             var c = exitem.item.cloneShallow(this.mBaseItem);
  119.             clonedExceptions.push( { id: exitem.id, item: c } );
  120.         }
  121.         cloned.mExceptions = clonedExceptions;
  122.  
  123.         return cloned;
  124.     },
  125.  
  126.     //
  127.     // calIRecurrenceInfo impl
  128.     //
  129.     mBaseItem: null,
  130.  
  131.     get item() {
  132.         return this.mBaseItem;
  133.     },
  134.  
  135.     set item(value) {
  136.         if (this.mImmutable)
  137.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  138.  
  139.         this.mBaseItem = value;
  140.         // patch exception's parentItem:
  141.         for each (exitem in this.mExceptions) {
  142.             exitem.item.parentItem = value;
  143.         }
  144.     },
  145.  
  146.     mRecurrenceItems: null,
  147.  
  148.     get isFinite() {
  149.         if (!this.mBaseItem)
  150.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  151.  
  152.         for each (ritem in this.mRecurrenceItems) {
  153.             if (!ritem.isFinite)
  154.                 return false;
  155.         }
  156.  
  157.         return true;
  158.     },
  159.  
  160.     getRecurrenceItems: function(aCount) {
  161.         if (!this.mBaseItem)
  162.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  163.  
  164.         aCount.value = this.mRecurrenceItems.length;
  165.         return this.mRecurrenceItems;
  166.     },
  167.  
  168.     setRecurrenceItems: function(aCount, aItems) {
  169.         if (!this.mBaseItem)
  170.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  171.  
  172.         if (this.mImmutable)
  173.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  174.  
  175.         // should we clone these?
  176.         this.mRecurrenceItems = aItems;
  177.     },
  178.  
  179.     countRecurrenceItems: function() {
  180.         if (!this.mBaseItem)
  181.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  182.  
  183.         return this.mRecurrenceItems.length;
  184.     },
  185.  
  186.     getRecurrenceItemAt: function(aIndex) {
  187.         if (!this.mBaseItem)
  188.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  189.  
  190.         if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length)
  191.             throw Components.results.NS_ERROR_INVALID_ARG;
  192.  
  193.         return this.mRecurrenceItems[aIndex];
  194.     },
  195.  
  196.     appendRecurrenceItem: function(aItem) {
  197.         if (!this.mBaseItem)
  198.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  199.  
  200.         if (this.mImmutable)
  201.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  202.  
  203.         this.mRecurrenceItems.push(aItem);
  204.     },
  205.  
  206.     deleteRecurrenceItemAt: function(aIndex) {
  207.         if (!this.mBaseItem)
  208.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  209.  
  210.         if (this.mImmutable)
  211.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  212.  
  213.         if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length)
  214.             throw Components.results.NS_ERROR_INVALID_ARG;
  215.  
  216.         this.mRecurrenceItems.splice(aIndex, 1);
  217.     },
  218.  
  219.     deleteRecurrenceItem: function(aItem) {
  220.         if (!this.mBaseItem)
  221.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  222.  
  223.         if (this.mImmutable)
  224.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  225.  
  226.         // Because xpcom objects can be wrapped in various ways, testing for
  227.         // mere == sometimes returns false even when it should be true.  Use
  228.         // the interface pointer returned by sip to avoid that problem.
  229.         var sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
  230.                             .createInstance(Components.interfaces.nsISupportsInterfacePointer);
  231.         sip1.data = aItem;
  232.         sip1.dataIID = Components.interfaces.calIRecurrenceItem;
  233.         for (var i = 0; i < this.mRecurrenceItems.length; i++) {
  234.             if (this.mRecurrenceItems[i] == sip1.data) {
  235.                 this.deleteRecurrenceItemAt(i);
  236.                 return;
  237.             }
  238.         }
  239.  
  240.         throw Components.results.NS_ERROR_INVALID_ARG;
  241.     },
  242.  
  243.     insertRecurrenceItemAt: function(aItem, aIndex) {
  244.         if (!this.mBaseItem)
  245.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  246.  
  247.         if (this.mImmutable)
  248.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  249.  
  250.         if (aIndex < 0 || aIndex > this.mRecurrenceItems.length)
  251.             throw Components.results.NS_ERROR_INVALID_ARG;
  252.  
  253.         this.mRecurrenceItems.splice(aIndex, 0, aItem);
  254.     },
  255.  
  256.     clearRecurrenceItems: function() {
  257.         if (!this.mBaseItem)
  258.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  259.  
  260.         if (this.mImmutable)
  261.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  262.  
  263.         this.mRecurrenceItems = new Array();
  264.     },
  265.  
  266.     //
  267.     // calculations
  268.     //
  269.  
  270.     getNextOccurrenceDate: function (aTime) {
  271.         if (!this.mBaseItem)
  272.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  273.  
  274.         var startDate = this.mBaseItem.recurrenceStartDate;
  275.         var dates = [];
  276.  
  277.         for each (ritem in this.mRecurrenceItems) {
  278.             var date = ritem.getNextOccurrence(startDate, aTime);
  279.             if (!date)
  280.                 continue;
  281.  
  282.             if (ritem.isNegative)
  283.                 dates = dates.filter(function (d) { return (d.compare(date) != 0); });
  284.             else
  285.                 dates.push(date);
  286.         }
  287.  
  288.         // if no dates, there's no next
  289.         if (dates.length == 0)
  290.             return null;
  291.  
  292.         // find the earliest date
  293.         var earliestDate = dates[0];
  294.         dates.forEach(function (d) { if (d.compare(earliestDate) < 0) earliestDate = d; });
  295.  
  296.         return earliestDate;
  297.     },
  298.  
  299.     getNextOccurrence: function (aTime) {
  300.         var earliestDate = this.getNextOccurrenceDate (aTime);
  301.         if (!earliestDate)
  302.             return null;
  303.  
  304.         if (this.mExceptions) {
  305.             // scan exceptions for any dates earlier than
  306.             // earliestDate (but still after aTime)
  307.             this.mExceptions.forEach (function (ex) {
  308.                                           var dtstart = ex.item.getProperty("DTSTART");
  309.                                           if (aTime.compare(dtstart) <= 0 &&
  310.                                               earliestDate.compare(dtstart) > 0)
  311.                                           {
  312.                                               earliestDate = dtstart;
  313.                                           }
  314.                                       });
  315.         }
  316.  
  317.         var startDate = earliestDate.clone();
  318.         var endDate = null;
  319.  
  320.         if (this.mBaseItem.hasProperty("DTEND")) {
  321.             endDate = earliestDate.clone();
  322.             endDate.addDuration(this.mBaseItem.duration);
  323.         }
  324.  
  325.         var proxy = this.mBaseItem.createProxy();
  326.         proxy.recurrenceId = earliestDate.clone();
  327.  
  328.         proxy.setProperty("DTSTART", startDate);
  329.         if (endDate)
  330.             proxy.setProperty("DTEND", endDate);
  331.  
  332.         return proxy;
  333.     },
  334.  
  335.     // internal helper function; 
  336.     calculateDates: function (aRangeStart, aRangeEnd,
  337.                               aMaxCount, aReturnRIDs)
  338.     {
  339.         if (!this.mBaseItem)
  340.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  341.  
  342.         // workaround for UTC- timezones
  343.         var rangeStart = ensureDateTime(aRangeStart);
  344.         var rangeEnd = ensureDateTime(aRangeEnd);
  345.  
  346.         // If aRangeStart falls in the middle of an occurrence, libical will
  347.         // not return that occurrence when we go and ask for an
  348.         // icalrecur_iterator_new.  This actually seems fairly rational, so 
  349.         // instead of hacking libical, I'm going to move aRangeStart back far
  350.         // enough to make sure we get the occurrences we might miss.
  351.         var searchStart = rangeStart.clone();
  352.         var baseDuration = null;
  353.         try {
  354.             baseDuration = this.mBaseItem.duration;
  355.             var duration = baseDuration.clone();
  356.             duration.isNegative = true;
  357.             searchStart.addDuration(duration);
  358.         } catch(ex) {
  359.             dump("recurrence tweaking exception:"+ex+'\n');
  360.         }
  361.  
  362.         var startDate = this.mBaseItem.recurrenceStartDate;
  363.         var dates = [];
  364.  
  365.         // DTSTART/DUE is always part of the (positive) expanded set:
  366.         // the base item cannot be replaced by an exception;
  367.         // an exception can only be defined on an item resulting from an RDATE/RRULE;
  368.         // DTSTART always equals RECURRENCE-ID for items expanded from RRULE
  369.         var baseOccDate = checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd, true);
  370.         if (baseOccDate) {
  371.             dates.push(baseOccDate);
  372.         }
  373.  
  374.         // toss in exceptions first:
  375.         if (this.mExceptions) {
  376.             this.mExceptions.forEach(
  377.                 function(ex) {
  378.                     var occDate = checkIfInRange(ex.item, aRangeStart, aRangeEnd, true);
  379.                     if (occDate) {
  380.                         dates.push(aReturnRIDs ? ex.id : occDate);
  381.                     }
  382.                 });
  383.         }
  384.  
  385.         // if both range start and end are specified, we ask for all of the occurrences,
  386.         // to make sure we catch all possible exceptions.  If aRangeEnd isn't specified,
  387.         // then we have to ask for aMaxCount, and hope for the best.
  388.         var maxCount;
  389.         if (rangeStart && rangeEnd) {
  390.             maxCount = 0;
  391.         } else {
  392.             maxCount = aMaxCount;
  393.         }
  394.  
  395.         // apply positive items before negative:
  396.         var sortedRecurrenceItems = [];
  397.         for each ( var ritem in this.mRecurrenceItems ) {
  398.             if (ritem.isNegative)
  399.                 sortedRecurrenceItems.push(ritem);
  400.             else
  401.                 sortedRecurrenceItems.unshift(ritem);
  402.         }
  403.         for each (ritem in sortedRecurrenceItems) {
  404.             var cur_dates;
  405.  
  406.             cur_dates = ritem.getOccurrences(startDate,
  407.                                              searchStart,
  408.                                              rangeEnd,
  409.                                              maxCount, {});
  410.  
  411.             if (cur_dates.length == 0)
  412.                 continue;
  413.  
  414.             if (ritem.isNegative) {
  415.                 // if this is negative, we look for any of the given dates
  416.                 // in the existing set, and remove them if they're
  417.                 // present.
  418.  
  419.                 // XXX: i'm pretty sure negative dates can't really have exceptions
  420.                 // (like, you can't make a date "real" by defining an RECURRENCE-ID which
  421.                 // is an EXDATE, and then giving it a real DTSTART) -- so we don't
  422.                 // check exceptions here
  423.                 cur_dates.forEach (function (dateToRemove) {
  424.                                        dates = dates.filter(function (d) { return d.compare(dateToRemove) != 0; });
  425.                                    });
  426.             } else {
  427.                 // if positive, we just add these date to the existing set,
  428.                 // but only if they're not already there
  429.  
  430.                 var index = 0;
  431.                 const len = cur_dates.length;
  432.  
  433.                 // skip items before rangeStart due to searchStart libical hack:
  434.                 if (rangeStart && baseDuration) {
  435.                     for (; index < len; ++index) {
  436.                         var date = cur_dates[index].clone();
  437.                         date.addDuration(baseDuration);
  438.                         if (rangeStart.compare(date) < 0) {
  439.                             break;
  440.                         }
  441.                     }
  442.                 }
  443.                 for (; index < len; ++index) {
  444.                     var dateToAdd = cur_dates[index];
  445.                     if (!dates.some(function (d) { return d.compare(dateToAdd) == 0; })) {
  446.                         dates.push(dateToAdd);
  447.                     }
  448.                 }
  449.             }
  450.         }
  451.  
  452.         // now sort the list
  453.         dates.sort(function (a,b) { return a.compare(b); });
  454.  
  455.         // chop anything over aMaxCount, if specified
  456.         if (aMaxCount && dates.length > aMaxCount)
  457.             dates = dates.splice(aMaxCount, dates.length - aMaxCount);
  458.  
  459.         return dates;
  460.     },
  461.  
  462.     getOccurrenceDates: function (aRangeStart, aRangeEnd,
  463.                                   aMaxCount, aCount)
  464.     {
  465.         var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, false);
  466.         aCount.value = dates.length;
  467.         return dates;
  468.     },
  469.  
  470.     getOccurrences: function (aRangeStart, aRangeEnd,
  471.                               aMaxCount,
  472.                               aCount)
  473.     {
  474.         var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, true);
  475.         if (dates.length == 0) {
  476.             aCount.value = 0;
  477.             return [];
  478.         }
  479.  
  480.         var count = aMaxCount;
  481.         if (!count)
  482.             count = dates.length;
  483.  
  484.         var results = [];
  485.  
  486.         for (var i = 0; i < count; i++) {
  487.             var proxy = this.getOccurrenceFor(dates[i]);
  488.             results.push(proxy);
  489.         }
  490.  
  491.         aCount.value = results.length;
  492.         return results;
  493.     },
  494.  
  495.     getOccurrenceFor: function (aRecurrenceId) {
  496.         var proxy = this.getExceptionFor(aRecurrenceId, false);
  497.         if (!proxy) {
  498.             var duration = null;
  499.             
  500.             var name = "DTEND";
  501.             if (this.mBaseItem instanceof Components.interfaces.calITodo)
  502.                 name = "DUE";
  503.                 
  504.             if (this.mBaseItem.hasProperty(name))
  505.                 duration = this.mBaseItem.duration;
  506.  
  507.             proxy = this.mBaseItem.createProxy();
  508.             proxy.recurrenceId = aRecurrenceId;
  509.             proxy.setProperty("DTSTART", aRecurrenceId.clone());
  510.             if (duration) {
  511.                 var enddate = aRecurrenceId.clone();
  512.                 enddate.addDuration(duration);
  513.                 proxy.setProperty(name, enddate);
  514.             }
  515.             if (!this.mBaseItem.isMutable)
  516.                 proxy.makeImmutable();
  517.         }
  518.         return proxy;
  519.     },
  520.  
  521.     removeOccurrenceAt: function (aRecurrenceId) {
  522.         if (!this.mBaseItem)
  523.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  524.  
  525.         if (this.mImmutable)
  526.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  527.  
  528.         var d = Components.classes["@mozilla.org/calendar/recurrence-date;1"].createInstance(Components.interfaces.calIRecurrenceDate);
  529.         d.isNegative = true;
  530.         d.date = aRecurrenceId.clone();
  531.  
  532.         this.removeExceptionFor(d.date);
  533.  
  534.         this.appendRecurrenceItem(d);
  535.     },
  536.  
  537.     restoreOccurrenceAt: function (aRecurrenceId) {
  538.         if (!this.mBaseItem)
  539.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  540.  
  541.         if (this.mImmutable)
  542.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  543.  
  544.         for (var i = 0; i < this.mRecurrenceItems.length; i++) {
  545.             if (this.mRecurrenceItems[i] instanceof Components.interfaces.calIRecurrenceDate) {
  546.                 var rd = this.mRecurrenceItems[i].QueryInterface(Components.interfaces.calIRecurrenceDate);
  547.                 if (rd.isNegative && rd.date.compare(aRecurrenceId) == 0) {
  548.                     return this.deleteRecurrenceItemAt(i);
  549.                 }
  550.             }
  551.         }
  552.  
  553.         throw Components.results.NS_ERROR_INVALID_ARG;
  554.     },
  555.  
  556.     //
  557.     // exceptions
  558.     //
  559.  
  560.     //
  561.     // Some notes:
  562.     //
  563.     // The way I read ICAL, RECURRENCE-ID is used to specify a
  564.     // particular instance of a recurring event, according to the
  565.     // RRULEs/RDATEs/etc. specified in the base event.  If one of
  566.     // these is to be changed ("an exception"), then it can be
  567.     // referenced via the UID of the original event, and a
  568.     // RECURRENCE-ID of the start time of the instance to change.
  569.     // This, to me, means that an event where one of the instances has
  570.     // changed to a different time has a RECURRENCE-ID of the original
  571.     // start time, and a DTSTART/DTEND representing the new time.
  572.     //
  573.     // ITIP, however, seems to want something different -- you're
  574.     // supposed to use UID/RECURRENCE-ID to select from the current
  575.     // set of occurrences of an event.  If you change the DTSTART for
  576.     // an instance, you're supposed to use the old (original) DTSTART
  577.     // as the RECURRENCE-ID, and put the new time as the DTSTART.
  578.     // However, after that change, to refer to that instance in the
  579.     // future, you have to use the modified DTSTART as the
  580.     // RECURRENCE-ID.  This madness is described in ITIP end of
  581.     // section 3.7.1.
  582.     // 
  583.     // This implementation does the first approach (RECURRENCE-ID will
  584.     // never change even if DTSTART for that instance changes), which
  585.     // I think is the right thing to do for CalDAV; I don't know what
  586.     // we'll do for incoming ITIP events though.
  587.     //
  588.  
  589.     mExceptions: null,
  590.  
  591.     modifyException: function (anItem) {
  592.         if (!this.mBaseItem)
  593.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  594.  
  595.         // the item must be an occurrence
  596.         if (anItem.parentItem == anItem)
  597.             throw Components.results.NS_ERROR_UNEXPECTED;
  598.  
  599.         if (anItem.parentItem.calendar != this.mBaseItem.calendar &&
  600.             anItem.parentItem.id != this.mBaseItem.id)
  601.         {
  602.             calDebug ("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!\n");
  603.             throw Components.results.NS_ERROR_INVALID_ARG;
  604.         }
  605.  
  606.         if (anItem.recurrenceId == null) {
  607.             calDebug ("recurrenceInfo::addException: item with null recurrenceId!\n");
  608.             throw Components.results.NS_ERROR_INVALID_ARG;
  609.         }
  610.  
  611.         var itemtoadd;
  612.         if (anItem.isMutable) {
  613.             itemtoadd = anItem.cloneShallow(this.mBaseItem);
  614.             itemtoadd.makeImmutable();
  615.         } else {
  616.             itemtoadd = anItem;
  617.         }
  618.  
  619.         // we're going to assume that the recurrenceId is valid here,
  620.         // because presumably the item came from one of our functions
  621.  
  622.         // remove any old one, if present
  623.         this.removeExceptionFor(anItem.recurrenceId);
  624.  
  625.         this.mExceptions.push( { id: itemtoadd.recurrenceId, item: itemtoadd } );
  626.     },
  627.  
  628.     createExceptionFor: function (aRecurrenceId) {
  629.         if (!this.mBaseItem)
  630.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  631.  
  632.         // XX should it be an error to createExceptionFor
  633.         // an already-existing recurrenceId?
  634.         var existing = this.getExceptionFor(aRecurrenceId, false);
  635.         if (existing)
  636.             return existing;
  637.  
  638.         // check if aRecurrenceId is valid.
  639.  
  640.         // this is a bit of a hack; we know that ranges are defined as [start, end),
  641.         // so we do a search on aRecurrenceId and aRecurrenceId.seconds + 1.
  642.         var rangeStart = aRecurrenceId;
  643.         var rangeEnd = aRecurrenceId.clone();
  644.         rangeEnd.second += 1;
  645.  
  646.         var dates = this.getOccurrenceDates (rangeStart, rangeEnd, 1, {});
  647.         var found = false;
  648.         for each (d in dates) {
  649.             if (d.compare(aRecurrenceId) == 0) {
  650.                 found = true;
  651.                 break;
  652.             }
  653.         }
  654.  
  655.         // not found; the recurrence id is invalid
  656.         if (!found)
  657.             throw Components.results.NS_ERROR_INVALID_ARG;
  658.  
  659.         var rid = aRecurrenceId.clone();
  660.         rid.makeImmutable();
  661.  
  662.         var newex = this.mBaseItem.createProxy();
  663.         newex.recurrenceId = rid;
  664.  
  665.         this.mExceptions.push({id: rid, item: newex});
  666.  
  667.         return newex;
  668.     },
  669.  
  670.     getExceptionFor: function (aRecurrenceId, aCreate) {
  671.         if (!this.mBaseItem)
  672.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  673.  
  674.         for each (ex in this.mExceptions) {
  675.             if (ex.id.compare(aRecurrenceId) == 0)
  676.                 return ex.item;
  677.         }
  678.  
  679.         if (aCreate) {
  680.             return this.createExceptionFor(aRecurrenceId);
  681.         }
  682.         return null;
  683.     },
  684.  
  685.     removeExceptionFor: function (aRecurrenceId) {
  686.         if (!this.mBaseItem)
  687.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  688.  
  689.         this.mExceptions = this.mExceptions.filter (function(ex) {
  690.                                                         return (ex.id.compare(aRecurrenceId) != 0);
  691.                                                     });
  692.     },
  693.  
  694.     getExceptionIds: function (aCount) {
  695.         if (!this.mBaseItem)
  696.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  697.  
  698.         var ids = this.mExceptions.map (function(ex) {
  699.                                             return ex.id;
  700.                                         });
  701.  
  702.         aCount.value = ids.length;
  703.         return ids;
  704.     },
  705.     
  706.     // changing the startdate of an item needs to take exceptions into account.
  707.     // in case we're about to modify a parentItem (aka 'folded' item), we need
  708.     // to modify the recurrenceId's of all possibly existing exceptions as well.
  709.     onStartDateChange: function (aNewStartTime, aOldStartTime) {
  710.  
  711.         // passing null for the new starttime would indicate an error condition,
  712.         // since having a recurrence without a starttime is invalid.
  713.         if (!aNewStartTime) {
  714.             throw Components.results.NS_ERROR_INVALID_ARG;
  715.         }
  716.     
  717.         // no need to check for changes if there's no previous starttime.
  718.         if (!aOldStartTime) {
  719.             return;
  720.         }
  721.     
  722.         // convert both dates to UTC since subtractDate is not timezone aware.
  723.         aOldStartTime = aOldStartTime.getInTimezone(UTC());
  724.         aNewStartTime = aNewStartTime.getInTimezone(UTC());
  725.         var timeDiff = aNewStartTime.subtractDate(aOldStartTime);
  726.         var exceptions = this.getExceptionIds({});
  727.         var modifiedExceptions = [];
  728.         for each (var exid in exceptions) {
  729.             var ex = this.getExceptionFor(exid, false);
  730.             if (ex) {
  731.                 if (!ex.isMutable) {
  732.                     ex = ex.cloneShallow(this.item);
  733.                 }
  734.                 ex.recurrenceId.addDuration(timeDiff);
  735.                 
  736.                 modifiedExceptions.push(ex);
  737.                 this.removeExceptionFor(exid);
  738.             }
  739.         }
  740.         for each (var modifiedEx in modifiedExceptions) {
  741.             this.modifyException(modifiedEx);
  742.         }
  743.  
  744.         // also take RDATE's and EXDATE's into account.
  745.         const kCalIRecurrenceDate = Components.interfaces.calIRecurrenceDate;
  746.         const kCalIRecurrenceDateSet = Components.interfaces.calIRecurrenceDateSet;
  747.         var ritems = this.getRecurrenceItems({});
  748.         for (var i in ritems) {
  749.             var ritem = ritems[i];
  750.             if (ritem instanceof kCalIRecurrenceDate) {
  751.                 ritem = ritem.QueryInterface(kCalIRecurrenceDate);
  752.                 ritem.date.addDuration(timeDiff);
  753.             } else if (ritem instanceof kCalIRecurrenceDateSet) {
  754.                 ritem = ritem.QueryInterface(kCalIRecurrenceDateSet);
  755.                 var rdates = ritem.getDates({});
  756.                 for each (var date in rdates) {
  757.                     date.addDuration(timeDiff);
  758.                 }
  759.                 ritem.setDates(rdates.length,rdates);
  760.             }
  761.         }
  762.     }
  763. };
  764.